package org.moonshot.chatty.lex;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

import org.moonshot.chatty.ChattyParseException;

public class ChattyLexer
{
    private static HashSet<Character> binopChars = new HashSet<Character> ();
    private static HashMap<Character, LexemeType> punct      = new HashMap<Character, LexemeType> ();
    static
    {
        final String binChars = "-+/\\*~!@%&<>=";
        for(int i = 0; i < binChars.length (); i++)
            binopChars.add (new Character (binChars.charAt (i)));
        
        punct.put (Character.valueOf('$'), LexemeType.LITARR);
        punct.put (Character.valueOf(','), LexemeType.COMMA);
        punct.put (Character.valueOf('.'), LexemeType.PERIOD);
        punct.put (Character.valueOf('|'), LexemeType.BAR);
        punct.put (Character.valueOf('('), LexemeType.OPAREN);
        punct.put (Character.valueOf(')'), LexemeType.CPAREN);
        punct.put (Character.valueOf('['), LexemeType.OBRACK);
        punct.put (Character.valueOf(']'), LexemeType.CBRACK);
        punct.put (Character.valueOf(':'), LexemeType.ASSIGN);
        punct.put (Character.valueOf('^'), LexemeType.RETURN);
    }
    
    private final ArrayList<ChattyLexeme> backpush = new ArrayList<ChattyLexeme> ();
    
    private final StringBuilder       value      = new StringBuilder (1024);
    private final String              input;
    private int                       pos        = 0;
    private int                       sPos       = 0;
    
    private ChattyLexeme last = null;
    
    public ChattyLexer(String aInput)
    {
        input = aInput;
        skipWhite ();
    }
    
    /**
     * Read the next lexeme; return null at end-of-document; throw an exception
     * on any problem.
     * @return
     * @throws ChattyParseException
     */
    public ChattyLexeme next () throws ChattyParseException
    {
        if( backpush.size () > 0 )
        {
            last = backpush.size () > 1 ? backpush.get (backpush.size () - 2) : null;
            return backpush.remove (backpush.size () - 1);
        }
        
        last = null;
        
        // Skip over whitespace and peek at the first character.
        skipWhite ();
        
        if( !valid () )
            return new ChattyLexeme(pos-2, pos-1, LexemeType.EOF);
        
        char first = at ();
        
        // Reset lexeme construction variables
        value.setLength (0);
        sPos = pos;
        
        try
        {
            // Select a lexeme type to parse.
            if( first == '"' )
                last = parseString ();
            else if( Character.isDigit (first) )
                last = parseNumber ('+');
            else if( Character.isLetter (first) )
                last = parseIdentOrFrag ();
            else if( binopChars.contains (new Character (first)) )
                last = parseBinopOrNumber ();
            else if( punct.containsKey (new Character (first)) )
                last = parsePunct (first);
        }
        catch (IndexOutOfBoundsException e)
        {
            throw new ChattyParseException (pos-1, pos, null, "Unexpected end of document");
        }
        catch (ChattyParseException e)
        {
        	throw e;
        }
        catch (Exception e)
        {
            throw new ChattyParseException (pos, pos+1, null, "Unexplained failure: "+e.getMessage ()+", "+e.getStackTrace ()[0].toString ());
        }
        
        if( last == null )
            throw new ChattyParseException (pos, pos+1, null, "Syntax error: " + name(at ()) + " unexpected");
        
        return last;
    }
    
    public void back (ChattyLexeme aLexeme)
    {
        backpush.add (aLexeme);
        last = aLexeme;
    }
    
    public ChattyLexeme peek ()
        throws ChattyParseException
    {
        ChattyLexeme theNode = next ();
        back(theNode);
        return theNode;
    }
    
    private String name(char c)
    {
        if( Character.isWhitespace (c) ) return "whitespace";
        if( Character.isDigit (c) ) return "digit";
        if( Character.isLetter (c) ) return "letter";
        if( c == '"' ) return "string quote";
        if( c == '\'') return "character quote";
        return "'"+c+"'";
    }
    
    /**
     * Parse punctuation
     * @param n
     * @return
     * @throws ChattyParseException
     */
    private ChattyLexeme parsePunct (char n)
        throws ChattyParseException
    {
    	eat ();
    	
    	if( n == '$' )
    	{
    		if( eat() != '(' )
    			throw new ChattyParseException (sPos, pos, null, "Syntax error: expected '$('");
    	}
    	else if( n == ':' )
    	{
            if( eat() != '=' )
                throw new ChattyParseException (sPos, pos, null, "Syntax error: expected ':='");
    	}
    	
        return new ChattyLexeme(sPos, pos, punct.get(new Character(n)));
    }
    
    /**
     * Parse a binary operator (or a number literal)
     * @return
     */
    private ChattyLexeme parseBinopOrNumber ()
    {
        char n = eat ();
        if( n == '-' || n == '+' && Character.isDigit (at ()) )
            return parseNumber (n);
        
        while(binopChars.contains (new Character (n)))
        {
            value.append (n);
            n = eat ();
        }
        return new ChattyLexeme (sPos, pos, LexemeType.BINOP, value.toString ());
    }
    
    /**
     * Parse an identifier or fragment; we won't know which until we reach the
     * :, if any.
     * @return
     */
    private ChattyLexeme parseIdentOrFrag ()
    {
        boolean frag = false;
        char n = eat();
        do {
            value.append (n);
        } while(Character.isLetter (n = eat()));
        
        if( n == ':' )
        {
            value.append (':');
            frag = true;
        }
        else
        {
            uneat ();
        }
        
        return new ChattyLexeme (sPos, pos, frag ? LexemeType.FRAGMENT : LexemeType.IDENT, value.toString ());
    }
    
    /**
     * Parse a number. Permit (but do not encode) commas as separators.
     * @param aSign
     * @return
     */
    private ChattyLexeme parseNumber (char aSign)
    {
        value.append (aSign);
        char n = eat ();
        while(Character.isDigit (n) || n == ',')
        {
            if( n != ',' )
                value.append (n);
            
            n = eat ();
        }
        
        if( n == '.' && (Character.isDigit(at()) || at() == ',') )
        {
            value.append ('.');
            n = eat ();
            
            while(Character.isDigit (n) || n == ',')
            {
                if( n != ',' )
                    value.append (n);
                
                n = eat ();
            }
        }
        
        uneat();
        
        return new ChattyLexeme (sPos, pos, LexemeType.NUMBER, value.toString ());
    }
    
    /**
     * Parse a string literal.
     * @return
     * @throws ChattyParseException
     */
    private ChattyLexeme parseString () throws ChattyParseException
    {
        eat ();
        char n = eat ();
        while(n != '"')
        {
            if( n == '\\' )
            {
                n = eat ();
                switch(n)
                {
                // Octal code
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                    int v = 0;
                    do
                    {
                        v = (v * 8) + (n - '0');
                        n = eat ();
                    } while(n >= '0' && n <= '7');
                    value.append ((char) v);
                    break;
                
                // Mnemonics
                case 'n':
                    value.append ("\n");
                    break;
                
                case 't':
                    value.append ("\t");
                    break;
                
                // Escaped literals
                case '"':
                case '\\':
                    value.append (n);
                    break;
                
                // Unrecognized
                default:
                    throw new ChattyParseException (pos-1, pos+1, null, "Unrecognized escape sequence");
                }
            }
            else
                value.append (n);
            
            n = eat ();
        }
        
        return new ChattyLexeme (sPos, pos, LexemeType.STRING, value.toString ());
    }
    
    private boolean valid ()
    {
        return pos < input.length ();
    }
    
    private void skipWhite ()
    {
        while(valid () && Character.isWhitespace (input.charAt (pos)))
            pos++;
    }
    
    private char at ()
    {
        return input.charAt (pos);
    }
    
    private char eat ()
    {
        return input.charAt (pos++);
    }
    
    private void uneat ()
    {
        pos--;
    }
}
